<?php

/**
 * Builds an ACL and enforces it on every request
 *
 * @author    Mon Zafra <monzee@gmail.com>
 * @package   Mz
 * @category  Helper
 * @license   New BSD
 * @version   $Id: AccessManager.php 38 2009-02-18 17:12:39Z monzee $
 */
class Mz_Helper_Action_AccessManager extends Zend_Controller_Action_Helper_Abstract
{
    protected $_options = array();
    protected $_defaults = array(
        'roles'    => array(),
        'login'    => array('module'    => 'default',
                            'controller'=> 'user',
                            'action'    => 'login'),
        'error'    => array('module'    => 'default',
                            'controller'=> 'error',
                            'action'    => 'denied'),
        'identity' => array('name'      => 'username',
                            'group'     => 'group',
                            'guest'     => 'guest'));
    /**
     * @var Zend_Acl
     */
    protected $_acl;
    /**
     * @var Zend_Controller_Request_Abstract
     */
    protected $_request;
    protected $_resource;

    /**
     * constructor
     *
     * valid options:
     *   'role'  => $roles,
     *   'login' => array('module'     => $moduleName,
     * 		              'controller' => $controllerName,
     * 		              'action'     => $actionName ),
     *   'error' => array('module'     => $moduleName,
     * 		              'controller' => $controllerName,
     * 		              'action'     => $actionName ),
     *   'identity' => array('name'  => $name,
     *                       'group' => $group,
     *                       'guest' => $guest )
     *
     * @param Zend_Acl          $acl     Zend_Acl instance
     * @param array|Zend_Config $options options
     */
    public function __construct(Zend_Acl $acl = null, $options = array())
    {
        $this->_acl = $acl;
        if ($options instanceof Zend_Config) {
            $options = $options->toArray();
        }
        $this->setOptions($options);
    }

    /**
     * overridden method
     * @return Core_Helper_Action_AccessManager
     */
    public function setActionController(Zend_Controller_Action $actionController = null)
    {
        $this->_actionController = $actionController;
        $this->_setProperties();
        return $this;
    }

    protected function _setProperties()
    {
        $this->_request = $this->_actionController->getRequest();
        if (isset($this->_actionController->resourceId)) {
            $this->_resource = $this->_actionController->resourceId;
        } else {
            $module = $this->_request->getModuleName();
            $controller = $this->_request->getControllerName();
            $this->_resource = $module . ':' . $controller;
        }
    }

    /**
     * @param  array $opts Options
     * @return Core_Helper_Action_AccessManager
     */
    public function setOptions(array $opts)
    {
        $defaults = $this->_defaults;
        foreach ($defaults as $key => $defaultVal) {
            if (isset($opts[$key])) {
                $this->_options[$key] = (array) $opts[$key] + $defaultVal;
            } else {
                $this->_options[$key] = $defaultVal;
            }
        }
        $this->_options = $this->_options + $opts;

        if (!empty($this->_options['roles'])) {
            $this->addRoles($this->_options['roles']);
        }

        return $this;
    }

    /**
     * @param  mixed $roles Roles to add
     * @param  bool  $reset Clear role registry first?
     * @return Core_Helper_Action_AccessManager
     */
    public function addRoles($roles, $reset=false)
    {
        $roles = (array) $roles;
        $acl = $this->getAcl();
        if ($reset) {
            $acl->removeRoleAll();
            $this->_options['roles'] = $roles;
        } else {
            $this->_options['roles'] += $roles;
        }
        if (!empty($roles)) {
            $this->_addRoles($roles);
        }
        return $this;
    }

    protected function _addRoles(array $roles, $parent=null)
    {
        $acl = $this->getAcl();
        foreach ($roles as $key => $val) {
            if (is_int($key) && !empty($val)) {
                $acl->hasRole($val) or $acl->addRole(new Zend_Acl_Role($val), $parent);
            }
            if (is_string($key)) {
                $acl->hasRole($key) or $acl->addRole(new Zend_Acl_Role($key), $parent);
                if (is_array($val)) {
                    $this->_addRoles($val, $key);
                } else if (!empty($val) && !$acl->hasrole($val)) {
                    $acl->addRole(new Zend_Acl_Role($val), $key);
                }
            }
        }
    }

    /**
     * @return Zend_Acl
     */
    public function getAcl()
    {
        if (!$this->_acl instanceof Zend_Acl) {
            $this->_acl = new Zend_Acl;
        }
        return $this->_acl;
    }

    /**
     * @return Core_Helper_Action_AccessManager
     */
    public function setAcl(Zend_Acl $acl)
    {
        $this->_acl = $acl;
        return $this;
    }

    public function preDispatch()
    {
        $this->buildPermissions();
        if ($this->isCurrentUserAllowed()) {
            return;
        }

        if (Zend_Auth::getInstance()->hasIdentity()) {
            $this->redirectTo403();
        } else {
            $this->redirectToLogin();
        }
    }

    /**
     * @todo   allow passing of assertions from allowGroups and allowUsers
     * @return Core_Helper_Action_AccessManager
     */
    public function buildPermissions()
    {
        $controller = $this->_actionController;
        $action = $this->_request->getActionName();
        $resId = $this->_resource;
        $auth = Zend_Auth::getInstance();
        $acl = $this->getAcl();
        $guestGroup = $this->_options['identity']['guest'];
        $nameCol = $this->_options['identity']['name'];

        // add current page to resources
        $acl->has($this->_resource) or $acl->add(new Zend_Acl_Resource($this->_resource));
        // add current user's name(md5'd to prevent collisions with group names)
        // and guest role to the role registry
        $acl->hasRole($guestGroup) or $acl->addRole(new Zend_Acl_Role($guestGroup));
        if ($auth->hasIdentity() && isset($auth->getIdentity()->$nameCol)) {
            $currentUser = md5($auth->getIdentity()->$nameCol);
            $acl->hasRole($currentUser) or $acl->addRole(new Zend_Acl_Role($currentUser));
        }

        if (isset($controller->allowGroups) || isset($controller->allowUsers)) {
            $mustAuthenticate = true;
        } else {
            $mustAuthenticate = isset($controller->mustAuthenticate)
                              ? $controller->mustAuthenticate : false;
        }

        if (!$mustAuthenticate) {
            // allow everyone if auth is not required
            $acl->allow(null, $resId);
        } else if ($auth->hasIdentity() && !isset($controller->allowGroups) &&
            !isset($controller->allowUsers))
        {
            // allow everyone except guests if auth is required but no group or
            // individuals are specified
            $acl->allow(null, $resId);
            $acl->deny($guestGroup, $resId);
        }

        if (isset($controller->allowGroups)) {
            $groups = (array) $controller->allowGroups;
            foreach ($groups as $key=>$value) {
                if (!is_string($key)) {
                    $acl->hasrole($value) or $acl->addRole(new Zend_Acl_Role($value));
                    $acl->allow($value, $resId);
                } else {
                    $acl->hasRole($key) or $acl->addRole(new Zend_Acl_Role($key));
                    $acl->allow($key, $resId, $value);
                }
            }
        }

        if (isset($controller->allowUsers) && $auth->hasIdentity()) {
            $users = (array) $controller->allowUsers;
            foreach ($users as $key=>$value) {
                if (!is_string($key)) {
                    $user = md5($value);
                    $acl->hasRole($user) or $acl->addRole(new Zend_Acl_Role($user));
                    $acl->allow($user, $resId);
                } else {
                    $user = md5($key);
                    $acl->hasRole($user) or $acl->addRole(new Zend_Acl_Role($user));
                    $acl->allow($user, $resId, $value);
                }
            }
        }

        return $this;
    }

    /**
     * @param  string $res    Resource identifier
     * @param  string $action Action name (e.g. 'index')
     * @return bool
     */
    public function isCurrentUserAllowed($res=null, $action=null)
    {
        $res = !empty($res) ? $res : $this->_resource;
        $action = !empty($action) ? $action : $this->_request->getActionName();
        $identity = Zend_Auth::getInstance()->getIdentity();
        $groupCol = $this->_options['identity']['group'];
        $nameCol = $this->_options['identity']['name'];
        $guestRole = $this->_options['identity']['guest'];
        $acl = $this->getAcl();

        if (isset($identity->$groupCol)) {
            $group = $identity->$groupCol;
            if ($acl->hasRole($group) && $acl->isAllowed($group, $res, $action)) {
                return true;
            }
        }

        $role = isset($identity->$nameCol) ? md5($identity->$nameCol) : $guestRole;

        return $acl->isAllowed($role, $res, $action);
    }

    public function redirectToLogin()
    {
        $flash = Zend_Controller_Action_HelperBroker::getStaticHelper('flashMessenger');
        $flash->setNamespace('error')
              ->addMessage('You must be authenticated first in order to access this resource.');
        $authPage = $this->_options['login'];
        $this->_request->setModuleName($authPage['module'])
                       ->setControllerName($authPage['controller'])
                       ->setActionName($authPage['action'])
                       ->setDispatched(false);
    }

    public function redirectTo403()
    {
        $flash = Zend_Controller_Action_HelperBroker::getStaticHelper('flashMessenger');
        $flash->setNamespace('error')
              ->addMessage('You do not have sufficient privileges to access this resource.');
        $errorPage = $this->_options['error'];
        $this->_request->setModuleName($errorPage['module'])
                       ->setControllerName($errorPage['controller'])
                       ->setActionName($errorPage['action'])
                       ->setDispatched(false);
    }

}